﻿using System;
using System.Collections.Generic;
using System.Linq;
using NodeSource.DataProvider;

namespace NodeSource.Executor
{
    public enum QueryCommandType
    {
        Select,
        Insert,
        Delete,
        From,
        Update,
        Join,
        On,
        Where,
        OrderBy,
        GroupBy
    }

    internal delegate bool QueryAppended(QueryCommandType queryCommandType, QueryEventArgs queryEventArgs);

    /// <summary>
    /// These all class names are according to their behavior and define dfa states
    /// </summary>
    public class FromQuery : Query
    {
        public readonly QuerySource QuerySource;

        internal FromQuery(QuerySource querySource)
            : base(null)
        {
            if (querySource.QuerySourceType == QuerySourceType.Node)
            {
                var nodeSource = querySource.GetSource() as NodeSource;
                if (nodeSource != null)
                {
                    AddSource(querySource.GetSource() as NodeSource);
                    Alias = nodeSource.Alias;
                }
            }
            else
            {
                var query = querySource.GetSource() as Query;
                if (query != null)
                {
                    AddSources(query.NodeSources);
                    Alias = query.Alias;
                }
            }

            QuerySource = querySource;
        }

        public SelectQuery Select(Action selectAction)
        {
            if (OnQueryAppended != null)
            {
                var queryEventArgs = new SelectQueryEventArgs
                                         {
                                             Query = this,
                                             SelectAction = selectAction
                                         };
                OnQueryAppended(QueryCommandType.Select, queryEventArgs);
                return new SelectQuery(this, queryEventArgs.SelectAction);
            }

            return new SelectQuery(this, selectAction);
        }

        public JoinQuery Join(QuerySource querySource, JoinType join)
        {
            if (NodeSourceType != querySource.NodeSourceType
                || !CouldBeGroupOfNodeSources)
            {
                var newQuery = From(querySource);
                OnQueryAppended += newQuery.ProcessQueryEvent;

                AddSplit(newQuery, join);
                return new JoinQuery(this, null, join);
            }

            return new JoinQuery(this, querySource, join);
        }
    }

    public class SelectQuery : Query
    {
        public readonly Query Query;
        public readonly Action SelectAction;

        public SelectQuery(Query query, Action selectAction)
            : base(query)
        {
            Query = query;
            SelectAction = selectAction;
        }

        public WhereQuery Where(Predicate predicate)
        {
            if (OnQueryAppended != null)
            {
                var queryEventArgs = new WhereQueryEventArgs
                {
                    Query = this,
                    Predicate = predicate
                };
                bool isConsumed = OnQueryAppended(QueryCommandType.Select, queryEventArgs);

                if (isConsumed)
                    return new WhereQuery(this, null);
            }

            return new WhereQuery(this, predicate);
        }

        public OrderQuery OrderBy(Order order)
        {
            if (OnQueryAppended != null)
            {
                var queryEventArgs = new OrderQueryEventArgs
                {
                    Query = this,
                    Order = order
                };
                bool isConsumed = OnQueryAppended(QueryCommandType.Select, queryEventArgs);

                if (isConsumed)
                    return new OrderQuery(this, null);
            }

            return new OrderQuery(this, order);
        }

        public GroupQuery GroupBy(GroupClause groupClause)
        {
            if (OnQueryAppended != null)
            {
                var queryEventArgs = new GroupQueryEventArgs
                {
                    Query = this,
                    GroupClause = groupClause
                };
                bool isConsumed = OnQueryAppended(QueryCommandType.Select, queryEventArgs);

                if (isConsumed)
                    return new GroupQuery(this, null);
            }

            return new GroupQuery(this, groupClause);
        }
    }

    public class WhereQuery : Query
    {
        public readonly Query Query;
        public readonly Predicate Predicate;

        internal WhereQuery(Query query, Predicate predicate)
            : base(query)
        {
            Query = query;
            Predicate = predicate;
        }

        public OrderQuery OrderBy(Order order)
        {
            if (OnQueryAppended != null)
            {
                var queryEventArgs = new OrderQueryEventArgs
                {
                    Query = this,
                    Order = order
                };
                bool isConsumed = OnQueryAppended(QueryCommandType.Select, queryEventArgs);

                if (isConsumed)
                    return new OrderQuery(this, null);
            }

            return new OrderQuery(this, order);
        }

        public GroupQuery GroupBy(GroupClause groupClause)
        {
            if (OnQueryAppended != null)
            {
                var queryEventArgs = new GroupQueryEventArgs
                {
                    Query = this,
                    GroupClause = groupClause
                };
                bool isConsumed = OnQueryAppended(QueryCommandType.Select, queryEventArgs);

                if (isConsumed)
                    return new GroupQuery(this, null);
            }

            return new GroupQuery(this, groupClause);
        }
    }

    public class GroupQuery : Query
    {
        public readonly Query Query;
        public readonly GroupClause GroupClause;
        public Predicate HavingClause;

        internal GroupQuery(Query query, GroupClause groupClause)
            : base(query)
        {
            Query = query;
            GroupClause = groupClause;
        }

        public Query Having(Predicate having)
        {
            HavingClause = having;
            return this;
        }
    }

    public class OrderQuery : Query
    {
        public readonly Query Query;
        public readonly Order Order;

        internal OrderQuery(Query query, Order order)
            : base(query)
        {
            Query = query;
            Order = order;
        }
    }

    public class OnQuery : Query
    {
        public readonly JoinQuery Query;
        public readonly Predicate Predicate;

        internal OnQuery(JoinQuery query, Predicate predicate)
            : base(query)
        {
            Query = query;
            Predicate = predicate;
        }

        public SelectQuery Select(Action selectAction)
        {
            if (OnQueryAppended != null)
            {
                var queryEventArgs = new SelectQueryEventArgs
                {
                    Query = this,
                    SelectAction = selectAction
                };
                OnQueryAppended(QueryCommandType.Select, queryEventArgs);
                return new SelectQuery(this, queryEventArgs.SelectAction);
            }

            return new SelectQuery(this, selectAction);
        }

        public JoinQuery Join(QuerySource querySource, JoinType join)
        {
            if (NodeSourceType != querySource.NodeSourceType
                || !CouldBeGroupOfNodeSources)
            {
                var newQuery = From(querySource);
                OnQueryAppended += newQuery.ProcessQueryEvent;

                AddSplit(newQuery, join);
                return new JoinQuery(this, null, join);
            }

            return new JoinQuery(this, querySource, join);
        }
    }

    public class JoinQuery : Query
    {
        public readonly Query Query;
        public readonly QuerySource TargetSource;
        public readonly JoinType JoinClause;

        internal JoinQuery(Query query, QuerySource targetSource, JoinType joinClause)
            : base(query)
        {
            Query = query;
            TargetSource = targetSource;
            JoinClause = joinClause;
        }

        public OnQuery On(Predicate expression)
        {
            if (OnQueryAppended != null)
            {
                var queryEventArgs = new OnQueryEventArgs
                {
                    Query = this,
                    Predicate = expression
                };
                bool isConsumed = OnQueryAppended(QueryCommandType.Select, queryEventArgs);

                if (isConsumed)
                    return new OnQuery(this, null);

                var splitHash = GetQuerySplitHashFromJoinPredicate(expression);
                if (Splits.ContainsKey(splitHash))
                {
                    Splits[splitHash].JoinPredicate = expression;
                    return new OnQuery(this, null);
                }
            }

            return new OnQuery(this, expression);
        }

        public SelectQuery Select(Action selectAction)
        {
            if (OnQueryAppended != null)
            {
                var queryEventArgs = new SelectQueryEventArgs
                {
                    Query = this,
                    SelectAction = selectAction
                };
                OnQueryAppended(QueryCommandType.Select, queryEventArgs);
                return new SelectQuery(this, queryEventArgs.SelectAction);
            }

            return new SelectQuery(this, selectAction);
        }

        public JoinQuery Join(QuerySource querySource, JoinType join)
        {
            if (NodeSourceType != querySource.NodeSourceType
                || !CouldBeGroupOfNodeSources)
            {
                var newQuery = From(querySource);
                OnQueryAppended += newQuery.ProcessQueryEvent;

                AddSplit(newQuery, join);
                return new JoinQuery(this, null, join);
            }

            return new JoinQuery(this, querySource, join);
        }
    }

    public class Query
    {
        internal QueryAppended OnQueryAppended;

        public String Alias;

        internal Dictionary<string, NodeSource> NodeSources = new Dictionary<string, NodeSource>();
        internal Dictionary<string, QuerySplit> Splits = new Dictionary<string, QuerySplit>();

        /// <summary>
        /// As nodes of one type can be queried together in a group
        /// </summary>
        internal DataProviderType NodeSourceType
        {
            get { return NodeSources.FirstOrDefault().Value.DataProviderType; }
        }

        /// <summary>
        /// Specifies if the data provider allows the nodes to be queried in group
        /// </summary>
        internal bool CouldBeGroupOfNodeSources
        {
            get { return NodeSources.FirstOrDefault().Value.CanBeQueriedInGroup; }
        }

        /// <summary>
        /// If the query is operating over multiple node sources
        /// </summary>
        internal bool IsGroupOfNodeSources
        {
            get { return CouldBeGroupOfNodeSources && NodeSources.Values.Count > 1; }
        }

        protected Query(Query query)
        {
            if (query != null)
            {
                NodeSources = query.NodeSources;
                Splits = query.Splits;
                OnQueryAppended = query.OnQueryAppended;
            }
        }

        public static FromQuery From(QuerySource querySource)
        {
            return new FromQuery(querySource);
        }

        public static FromQuery From(NodeSource nodeSource)
        {
            return new FromQuery(new QuerySource(nodeSource));
        }

        public static FromQuery From(String alias, Query query)
        {
            return new FromQuery(new QuerySource(alias, query));
        }

        protected void AddSource(NodeSource nodeSource)
        {
            if (NodeSources.ContainsKey(nodeSource.Alias))
                throw new Exception(String.Format("Bad Query! NodeSource '{0}' with alias '{1}' is already included in query", nodeSource.Name, nodeSource.Alias));

            NodeSources.Add(nodeSource.Alias, nodeSource);
        }

        protected void AddSources(Dictionary<string, NodeSource> nodeSources)
        {
            foreach (var nodeSourceKeyValuePair in nodeSources)
            {
                AddSource(nodeSourceKeyValuePair.Value);
            }
        }

        internal static string GetQuerySplitHash(string firstSourceAlias, string secondSourceAlias)
        {
            if (firstSourceAlias[0] < secondSourceAlias[0])
                return firstSourceAlias + "-" + secondSourceAlias;
            return secondSourceAlias + "-" + firstSourceAlias;
        }

        internal static string GetQuerySplitHashFromJoinPredicate(Predicate predicate)
        {
            var binaryExpr = predicate.Expression as BinaryExpression;

            if (binaryExpr == null)
                return String.Empty;

            String leftAliasHandle = String.Empty;
            String rightAliasHandle = String.Empty;

            if (binaryExpr.LeftOperand is ParameterExpression)
            {
                var leftParam = binaryExpr.LeftOperand as ParameterExpression;
                leftAliasHandle = leftParam.Name.Split('.')[0];
            }

            if (binaryExpr.RightOperand is ParameterExpression)
            {
                var rightParam = binaryExpr.RightOperand as ParameterExpression;
                rightAliasHandle = rightParam.Name.Split('.')[0];
            }

            return GetQuerySplitHash(leftAliasHandle, rightAliasHandle);
        }

        internal static int GetExpressionGravity(Expression expression, Query leftQuery, Query rightQuery)
        {
            int leftRightGravity = 0;
            var binaryExpr = expression as BinaryExpression;

            if (binaryExpr == null)
                return leftRightGravity;

            if (binaryExpr.LeftOperand is ParameterExpression)
            {
                var leftParam = binaryExpr.LeftOperand as ParameterExpression;
                var aliasHandle = leftParam.Name.Split('.')[0];

                if (leftQuery.NodeSources.ContainsKey(aliasHandle))
                    leftRightGravity--;
                else
                    leftRightGravity++;
            }

            if (binaryExpr.RightOperand is ParameterExpression)
            {
                var rightParam = binaryExpr.LeftOperand as ParameterExpression;
                if (rightParam != null)
                {
                    var aliasHandle = rightParam.Name.Split('.')[0];

                    if (leftQuery.NodeSources.ContainsKey(aliasHandle))
                        leftRightGravity--;
                    else
                        leftRightGravity++;
                }
            }

            return leftRightGravity;

        }

        protected void AddSplit(Query secondQuery, JoinType joinType)
        {
            Splits.Add(GetQuerySplitHash(Alias, secondQuery.Alias), new QuerySplit
            {
                Query = secondQuery,
                SplitJoinType = joinType
            });
        }

        internal bool ProcessQueryEvent(QueryCommandType queryCommandType, QueryEventArgs queryEventArgs)
        {
            return QueryEventProcessor.ProcessQueryEvent(this, queryCommandType, queryEventArgs);
        }
    }
}